<html>
<head>
<meta charset="utf8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
<title>WebGL 3D Lathe Compute Normals</title>
<link type="text/css" href="resources/webgl-tutorials.css" rel="stylesheet" />
</head>
<body>
<canvas></canvas>
<div id="uiContainer">
  <div id="ui">
  </div>
</div>
</body>
<!-- vertex shader -->
<script id="simple-vertex-shader" type="notjs">
attribute vec4 a_position;
attribute vec2 a_texcoord;
attribute vec3 a_normal;

uniform mat4 u_matrix;

varying vec2 v_texcoord;
varying vec3 v_normal;

void main() {
  // Multiply the position by the matrix.
  gl_Position = u_matrix * a_position;

  // Pass the texcoord to the fragment shader.
  v_texcoord = a_texcoord;
  v_normal = a_normal;
}
</script>
<script id="specular-vertex-shader" type="notjs">
attribute vec4 a_position;
attribute vec3 a_normal;

uniform vec3 u_lightWorldPosition;
uniform vec3 u_viewWorldPosition;

uniform mat4 u_world;
uniform mat4 u_worldViewProjection;
uniform mat4 u_worldInverseTranspose;

varying vec3 v_normal;
varying vec3 v_surfaceToLight;
varying vec3 v_surfaceToView;

void main() {
  // Multiply the position by the matrix.
  gl_Position = u_worldViewProjection * a_position;

  // orient the normals and pass to the fragment shader
  v_normal = mat3(u_worldInverseTranspose) * a_normal;

  // compute the world position of the surfoace
  vec3 surfaceWorldPosition = (u_world * a_position).xyz;

  // compute the vector of the surface to the light
  // and pass it to the fragment shader
  v_surfaceToLight = u_lightWorldPosition - surfaceWorldPosition;

  // compute the vector of the surface to the view/camera
  // and pass it to the fragment shader
  v_surfaceToView = u_viewWorldPosition - surfaceWorldPosition;
}
</script>
<!-- fragment shader -->
<script id="normal-shader" type="notjs">
precision mediump float;

// Passed in from the vertex shader.
varying vec2 v_texcoord;
varying vec3 v_normal;

uniform sampler2D u_texture;

void main() {
  gl_FragColor = vec4(v_normal * .5 + .5, 1);
}
</script>
<script id="texture-shader" type="notjs">
precision mediump float;

// Passed in from the vertex shader.
varying vec2 v_texcoord;
varying vec3 v_normal;

uniform sampler2D u_texture;

void main() {
  gl_FragColor = texture2D(u_texture, v_texcoord);
}
</script>
<script id="specular-shader" type="notjs">
#if GL_FRAGMENT_PRECISION_HIGH
  precision highp float;
#else
  precision mediump float;
#endif

// Passed in from the vertex shader.
varying vec3 v_normal;
varying vec3 v_surfaceToLight;
varying vec3 v_surfaceToView;

uniform vec4 u_color;
uniform float u_shininess;

void main() {
  // because v_normal is a varying it's interpolated
  // so it will not be a unit vector. Normalizing it
  // will make it a unit vector again
  vec3 normal = normalize(v_normal);
  vec3 surfaceToLightDirection = normalize(v_surfaceToLight);
  vec3 surfaceToViewDirection = normalize(v_surfaceToView);
  vec3 halfVector = normalize(surfaceToLightDirection + surfaceToViewDirection);

  float light = dot(normal, surfaceToLightDirection);
  float specular = 0.0;
  if (light > 0.0) {
    specular = pow(dot(normal, halfVector), u_shininess);
  }

  gl_FragColor = u_color;

  // Lets multiply just the color portion (not the alpha)
  // by the light
  gl_FragColor.rgb *= light;

  // Just add in the specular
  gl_FragColor.rgb += specular;
}
</script>
<!-- this is included only for iframe and requestAnimationFrame support -->
<!--
for most samples webgl-utils only provides shader compiling/linking and
canvas resizing because why clutter the examples with code that's the same in every sample.
See https://webglfundamentals.org/webgl/lessons/webgl-boilerplate.html
and https://webglfundamentals.org/webgl/lessons/webgl-resizing-the-canvas.html
for webgl-utils, m3, m4, and webgl-lessons-ui.
-->
<script src="resources/webgl-lessons-ui.js"></script>
<script src="resources/webgl-utils.js"></script>
<script src="resources/3d-math.js"></script>
<script src="resources/2d-math.js"></script>
<script>
"use strict";

//const svg = "m44,434c18,-33 19,-66 15,-111c-4,-45 -37,-104 -39,-132c-2,-28 11,-51 16,-81c5,-30 3,-63 -36,-63";
const svg =
"M236,124L197,112L197,34C197,34 184.859,31.871 165,33C186.997,66.892 161.894,89.627 173,109C184.106,128.373 186.493,137.68 205,144C219.37,148.907 222,154 222,154L220,175L202,174L191,194L204,209L222,208C222,208 226.476,278.566 218,295C209.524,311.434 191.013,324.945 201,354C210.987,383.055 213,399 213,399L191,403L191,417L212,422C212,422 233.283,437.511 211,444C188.717,450.489 111,472 111,472L111,485L236,484L236,124Z";

function main() {
  const opt = getQueryParams();
  const curvePoints = parseSVGPath(svg, { xFlip: true });

  // Get A WebGL context
  /** @type {HTMLCanvasElement} */
  const canvas = document.querySelector("canvas");
  const gl = canvas.getContext("webgl");
  if (!gl) {
    return;
  }

  const data = {
    tolerance: 0.15,
    distance: .4,
    divisions: 16,
    startAngle: 0,
    endAngle: Math.PI * 2,
    capStart: true,
    capEnd: true,
    triangles: true,
    maxAngle: degToRad(30),
    mode: 0,
  };

  function generateMesh(bufferInfo) {
    const tempPoints = getPointsOnBezierCurves(curvePoints, data.tolerance);
    const points = simplifyPoints(tempPoints, 0, tempPoints.length, data.distance);
    const tempArrays = lathePoints(points, data.startAngle, data.endAngle, data.divisions, data.capStart, data.capEnd);
    const arrays = generateNormals(tempArrays, data.maxAngle);
    const extents = getExtents(arrays.position);
    if (!bufferInfo) {
      // calls gl.createBuffer, gl.bindBuffer, and gl.bufferData for each array
      bufferInfo = webglUtils.createBufferInfoFromArrays(gl, arrays);
    } else {
      gl.bindBuffer(gl.ARRAY_BUFFER, bufferInfo.attribs.a_position.buffer);
      gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(arrays.position), gl.STATIC_DRAW);
      gl.bindBuffer(gl.ARRAY_BUFFER, bufferInfo.attribs.a_texcoord.buffer);
      gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(arrays.texcoord), gl.STATIC_DRAW);
      gl.bindBuffer(gl.ARRAY_BUFFER, bufferInfo.attribs.a_normal.buffer);
      gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(arrays.normal), gl.STATIC_DRAW);
      gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, bufferInfo.indices);
      gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(arrays.indices), gl.STATIC_DRAW);
      bufferInfo.numElements = arrays.indices.length;
    }
    return {
      bufferInfo: bufferInfo,
      extents: extents,
    };
  }

  // used to force the locations of attributes so they are
  // the same across programs
  const attributes = [ 'a_position', 'a_texcoord', 'a_normal' ];
  // setup GLSL program
  const programInfos = [
    // compiles shaders, links program and looks up locations
    webglUtils.createProgramInfo(gl, ["simple-vertex-shader", "normal-shader"], attributes),
    webglUtils.createProgramInfo(gl, ["specular-vertex-shader", "specular-shader"], attributes),
    webglUtils.createProgramInfo(gl, ["simple-vertex-shader", "texture-shader"], attributes),
  ];

  const texInfo = loadImageAndCreateTextureInfo("resources/uv-grid.png", render);

  let worldMatrix = m4.identity();
  let projectionMatrix;
  let extents;
  let bufferInfo;

  function update() {
    const info = generateMesh(bufferInfo);
    extents = info.extents;
    bufferInfo = info.bufferInfo;
    render();
  }
  update();

  function render() {
    webglUtils.resizeCanvasToDisplaySize(gl.canvas, window.devicePixelRatio);

    // Tell WebGL how to convert from clip space to pixels
    gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);

    gl.enable(gl.DEPTH_TEST);

    // Clear the canvas AND the depth buffer.
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

    // Compute the projection matrix
    const fieldOfViewRadians = Math.PI * .25;
    const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
    projectionMatrix =
        m4.perspective(fieldOfViewRadians, aspect, 1, 2000);

    // Compute the camera's matrix using look at.
    const midY = lerp(extents.min[1], extents.max[1], .5);
    const sizeToFitOnScreen = (extents.max[1] - extents.min[1]) * .6;
    const distance = sizeToFitOnScreen / Math.tan(fieldOfViewRadians * .5);
    const cameraPosition = [0, midY, distance];
    const target = [0, midY, 0];
    const up = [0, -1, 0];  // we used 2d points as input which means orientation is flipped
    const cameraMatrix = m4.lookAt(cameraPosition, target, up);

    // Make a view matrix from the camera matrix.
    const viewMatrix = m4.inverse(cameraMatrix);

    const viewProjectionMatrix = m4.multiply(projectionMatrix, viewMatrix);

    const programInfo = programInfos[data.mode];
    gl.useProgram(programInfo.program);

    // Setup all the needed attributes.
    // calls gl.bindBuffer, gl.enableVertexAttribArray, gl.vertexAttribPointer for each attribute
    webglUtils.setBuffersAndAttributes(gl, programInfo, bufferInfo);

    const worldViewProjection = m4.multiply(viewProjectionMatrix, worldMatrix);

    // Set the uniforms
    // calls gl.uniformXXX, gl.activeTexture, gl.bindTexture
    webglUtils.setUniforms(programInfo, {
      u_matrix: worldViewProjection,
      u_worldViewProjection: worldViewProjection,
      u_world: worldMatrix,
      u_worldInverseTranspose: m4.transpose(m4.inverse(worldMatrix)),
      u_lightWorldPosition: [midY * 1.5, midY * 2, distance * 1.5],
      u_viewWorldPosition: cameraMatrix.slice(12, 15),
      u_color: [1, 0.8, 0.2, 1],
      u_shininess: 50,
      u_texture: texInfo.texture,
    });

    // calls gl.drawArrays or gl.drawElements.
    webglUtils.drawBufferInfo(gl, bufferInfo, data.triangles ? gl.TRIANGLE : gl.LINES);
  }

  function getExtents(positions) {
    const min = positions.slice(0, 3);
    const max = positions.slice(0, 3);
    for (let i = 3; i < positions.length; i += 3) {
      min[0] = Math.min(positions[i + 0], min[0]);
      min[1] = Math.min(positions[i + 1], min[1]);
      min[2] = Math.min(positions[i + 2], min[2]);
      max[0] = Math.max(positions[i + 0], max[0]);
      max[1] = Math.max(positions[i + 1], max[1]);
      max[2] = Math.max(positions[i + 2], max[2]);
    }
    return {
      min: min,
      max: max,
    };
  }

  // get the points from an SVG path. assumes a continous line
  function parseSVGPath(svg, opt) {
    const points = [];
    let delta = false;
    let keepNext = false;
    let need = 0;
    let value = '';
    let values = [];
    let lastValues = [0, 0];
    let nextLastValues = [0, 0];
    let mode;

    function addValue() {
      if (value.length > 0) {
        const v = parseFloat(value);
        if (v > 1000) {
          debugger;  // eslint-disable-line
        }
        values.push(v);
        if (values.length === 2) {
          if (delta) {
            values[0] += lastValues[0];
            values[1] += lastValues[1];
          }
          points.push(values);
          if (keepNext) {
            nextLastValues = values.slice();
          }
          --need;
          if (!need) {
            if (mode === 'l') {
              const m4 = points.pop();
              const m1 = points.pop();
              const m2 = v2.lerp(m1, m4, 0.25);
              const m3 = v2.lerp(m1, m4, 0.75);
              points.push(m1, m2, m3, m4);
            }
            lastValues = nextLastValues;
          }
          values = [];
        }
        value = '';
      }
    }

    svg.split('').forEach((c, ndx) => {
      if ((c >= '0' && c <= '9') || c === '.') {
        value += c;
      } else if (c === '-') {
        addValue();
        value = '-';
      } else if (c === 'm') {
        addValue();
        keepNext = true;
        need = 1;
        delta = true;
        mode = 'm';
      } else if (c === 'c') {
        addValue();
        keepNext = true;
        need = 3;
        delta = true;
        mode = 'c';
      } else if (c === 'l') {
        addValue();
        keepNext = true;
        need = 1;
        delta = false;
        mode = 'l';
      } else if (c === 'M') {
        addValue();
        keepNext = true;
        need = 1;
        delta = false;
        mode = 'm';
      } else if (c === 'C') {
        addValue();
        keepNext = true;
        need = 3;
        delta = false;
        mode = 'c';
      } else if (c === 'L') {
        addValue();
        keepNext = true;
        need = 1;
        delta = false;
        mode = 'l';
      } else if (c === 'Z') {
        // close the loop
      } else if (c === ',') {
        addValue();
      } else if (c === ' ') {
        addValue();
      } else {
        debugger;  // eslint-disable-line
      }
    });
    addValue();
    let min = points[0].slice();
    let max = points[0].slice();
    for (let i = 1; i < points.length; ++i) {
      min = v2.min(min, points[i]);
      max = v2.max(max, points[i]);
    }
    let range = v2.sub(max, min);
    let halfRange = v2.mult(range, .5);
    for (let i = 0; i < points.length; ++i) {
      const p = points[i];
      if (opt.xFlip) {
        p[0] = max[0] - p[0];
      } else {
        p[0] = p[0] - min[0];
      }
      p[1] = (p[1] - min[0]) - halfRange[1];
    }
    return points;
  }

  function getPointOnBezierCurve(points, offset, t) {
    const invT = (1 - t);
    return v2.add(v2.mult(points[offset + 0], invT * invT * invT),
                  v2.mult(points[offset + 1], 3 * t * invT * invT),
                  v2.mult(points[offset + 2], 3 * invT * t * t),
                  v2.mult(points[offset + 3], t * t * t));
  }

  function getPointsOnBezierCurve(points, offset, numPoints) {
    const cpoints = [];
    for (let i = 0; i < numPoints; ++i) {
      const t = i / (numPoints - 1);
      cpoints.push(getPointOnBezierCurve(points, offset, t));
    }
    return cpoints;
  }

  function flatness(points, offset) {
    const p1 = points[offset + 0];
    const p2 = points[offset + 1];
    const p3 = points[offset + 2];
    const p4 = points[offset + 3];

    let ux = 3 * p2[0] - 2 * p1[0] - p4[0]; ux *= ux;
    let uy = 3 * p2[1] - 2 * p1[1] - p4[1]; uy *= uy;
    let vx = 3 * p3[0] - 2 * p4[0] - p1[0]; vx *= vx;
    let vy = 3 * p3[1] - 2 * p4[1] - p1[1]; vy *= vy;

    if (ux < vx) {
      ux = vx;
    }

    if (uy < vy) {
      uy = vy;
    }

    return ux + uy;
  }

  function getPointsOnBezierCurveWithSplitting(points, offset, tolerance, newPoints) {
    const outPoints = newPoints || [];
    if (flatness(points, offset) < tolerance) {

      // just add the end points of this curve
      outPoints.push(points[offset + 0]);
      outPoints.push(points[offset + 3]);

    } else {

      // subdivide
      const t = .5;
      const p1 = points[offset + 0];
      const p2 = points[offset + 1];
      const p3 = points[offset + 2];
      const p4 = points[offset + 3];

      const q1 = v2.lerp(p1, p2, t);
      const q2 = v2.lerp(p2, p3, t);
      const q3 = v2.lerp(p3, p4, t);

      const r1 = v2.lerp(q1, q2, t);
      const r2 = v2.lerp(q2, q3, t);

      const red = v2.lerp(r1, r2, t);

      // do 1st half
      getPointsOnBezierCurveWithSplitting([p1, q1, r1, red], 0, tolerance, outPoints);
      // do 2nd half
      getPointsOnBezierCurveWithSplitting([red, r2, q3, p4], 0, tolerance, outPoints);

    }
    return outPoints;
  }

  // gets points across all segments
  function getPointsOnBezierCurves(points, tolerance) {
    const newPoints = [];
    const numSegments = (points.length - 1) / 3;
    for (let i = 0; i < numSegments; ++i) {
      const offset = i * 3;
      getPointsOnBezierCurveWithSplitting(points, offset, tolerance, newPoints);
    }
    return newPoints;
  }

  // Ramer Douglas Peucker algorithm
  function simplifyPoints(points, start, end, epsilon, newPoints) {
    const outPoints = newPoints || [];

    // find the most distant point from the line formed by the endpoints
    const s = points[start];
    const e = points[end - 1];
    let maxDistSq = 0;
    let maxNdx = 1;
    for (let i = start + 1; i < end - 1; ++i) {
      const distSq = v2.distanceToSegmentSq(points[i], s, e);
      if (distSq > maxDistSq) {
        maxDistSq = distSq;
        maxNdx = i;
      }
    }

    // if that point is too far
    if (Math.sqrt(maxDistSq) > epsilon) {

      // split
      simplifyPoints(points, start, maxNdx + 1, epsilon, outPoints);
      simplifyPoints(points, maxNdx, end, epsilon, outPoints);

    } else {

      // add the 2 end points
      outPoints.push(s, e);
    }

    return outPoints;
  }

  // rotates around Y axis.
  function lathePoints(points,
                       startAngle,   // angle to start at (ie 0)
                       endAngle,     // angle to end at (ie Math.PI * 2)
                       numDivisions, // how many quads to make around
                       capStart,     // true to cap the top
                       capEnd) {     // true to cap the bottom
    const positions = [];
    const texcoords = [];
    const indices = [];

    const vOffset = capStart ? 1 : 0;
    const pointsPerColumn = points.length + vOffset + (capEnd ? 1 : 0);
    const quadsDown = pointsPerColumn - 1;

    // generate v coordniates
    let vcoords = [];

    // first compute the length of the points
    let length = 0;
    for (let i = 0; i < points.length - 1; ++i) {
      vcoords.push(length);
      length += v2.distance(points[i], points[i + 1]);
    }
    vcoords.push(length);  // the last point

    // now divide each by the total length;
    vcoords = vcoords.map(v => v / length);

    // generate points
    for (let division = 0; division <= numDivisions; ++division) {
      const u = division / numDivisions;
      const angle = lerp(startAngle, endAngle, u) % (Math.PI * 2);
      const mat = m4.yRotation(angle);
      if (capStart) {
        // add point on Y access at start
        positions.push(0, points[0][1], 0);
        texcoords.push(u, 0);
      }
      points.forEach((p, ndx) => {
        const tp = m4.transformPoint(mat, [...p, 0]);
        positions.push(tp[0], tp[1], tp[2]);
        texcoords.push(u, vcoords[ndx]);
      });
      if (capEnd) {
        // add point on Y access at end
        positions.push(0, points[points.length - 1][1], 0);
        texcoords.push(u, 1);
      }
    }

    // generate indices
    for (let division = 0; division < numDivisions; ++division) {
      const column1Offset = division * pointsPerColumn;
      const column2Offset = column1Offset + pointsPerColumn;
      for (let quad = 0; quad < quadsDown; ++quad) {
        indices.push(column1Offset + quad, column1Offset + quad + 1, column2Offset + quad);
        indices.push(column1Offset + quad + 1, column2Offset + quad + 1, column2Offset + quad);
      }
    }

    return {
      position: positions,
      texcoord: texcoords,
      indices: indices,
    };
  }

  function makeIndexedIndicesFn(arrays) {
    const indices = arrays.indices;
    let ndx = 0;
    const fn = function() {
      return indices[ndx++];
    };
    fn.reset = function() {
      ndx = 0;
    };
    fn.numElements = indices.length;
    return fn;
  }

  function makeUnindexedIndicesFn(arrays) {
    let ndx = 0;
    const fn = function() {
      return ndx++;
    };
    fn.reset = function() {
      ndx = 0;
    };
    fn.numElements = arrays.positions.length / 3;
    return fn;
  }

  function makeIndiceIterator(arrays) {
    return arrays.indices
        ? makeIndexedIndicesFn(arrays)
        : makeUnindexedIndicesFn(arrays);
  }

  function generateNormals(arrays, maxAngle) {
    const positions = arrays.position;
    const texcoords = arrays.texcoord;

    // first compute the normal of each face
    let getNextIndex = makeIndiceIterator(arrays);
    const numFaceVerts = getNextIndex.numElements;
    const numVerts = arrays.position.length;
    const numFaces = numFaceVerts / 3;
    const faceNormals = [];

    // Compute the normal for every face.
    // While doing that, create a new vertex for every face vertex
    for (let i = 0; i < numFaces; ++i) {
      const n1 = getNextIndex() * 3;
      const n2 = getNextIndex() * 3;
      const n3 = getNextIndex() * 3;

      const v1 = positions.slice(n1, n1 + 3);
      const v2 = positions.slice(n2, n2 + 3);
      const v3 = positions.slice(n3, n3 + 3);

      faceNormals.push(m4.normalize(m4.cross(m4.subtractVectors(v1, v2), m4.subtractVectors(v3, v2))));
    }

    let tempVerts = {};
    let tempVertNdx = 0;

    // this assumes vertex positions are an exact match

    function getVertIndex(x, y, z) {

      const vertId = x + "," + y + "," + z;
      const ndx = tempVerts[vertId];
      if (ndx !== undefined) {
        return ndx;
      }
      const newNdx = tempVertNdx++;
      tempVerts[vertId] = newNdx;
      return newNdx;
    }

    // We need to figure out the shared vertices.
    // It's not as simple as looking at the faces (triangles)
    // because for example if we have a standard cylinder
    //
    //
    //      3-4
    //     /   \
    //    2     5   Looking down a cylinder starting at S
    //    |     |   and going around to E, E and S are not
    //    1     6   the same vertex in the data we have
    //     \   /    as they don't share UV coords.
    //      S/E
    //
    // the vertices at the start and end do not share vertices
    // since they have different UVs but if you don't consider
    // them to share vertices they will get the wrong normals

    const vertIndices = [];
    for (let i = 0; i < numVerts; ++i) {
      const offset = i * 3;
      const vert = positions.slice(offset, offset + 3);
      vertIndices.push(getVertIndex(vert));
    }

    // go through every vertex and record which faces it's on
    const vertFaces = [];
    getNextIndex.reset();
    for (let i = 0; i < numFaces; ++i) {
      for (let j = 0; j < 3; ++j) {
        const ndx = getNextIndex();
        const sharedNdx = vertIndices[ndx];
        let faces = vertFaces[sharedNdx];
        if (!faces) {
          faces = [];
          vertFaces[sharedNdx] = faces;
        }
        faces.push(i);
      }
    }

    // now go through every face and compute the normals for each
    // vertex of the face. Only include faces that aren't more than
    // maxAngle different. Add the result to arrays of newPositions,
    // newTexcoords and newNormals, discarding any vertices that
    // are the same.
    tempVerts = {};
    tempVertNdx = 0;
    const newPositions = [];
    const newTexcoords = [];
    const newNormals = [];

    function getNewVertIndex(x, y, z, nx, ny, nz, u, v) {
      const vertId =
          x + "," + y + "," + z + "," +
          nx + "," + ny + "," + nz + "," +
          u + "," + v;

      const ndx = tempVerts[vertId];
      if (ndx !== undefined) {
        return ndx;
      }
      const newNdx = tempVertNdx++;
      tempVerts[vertId] = newNdx;
      newPositions.push(x, y, z);
      newNormals.push(nx, ny, nz);
      newTexcoords.push(u, v);
      return newNdx;
    }

    const newVertIndices = [];
    getNextIndex.reset();
    const maxAngleCos = Math.cos(maxAngle);
    // for each face
    for (let i = 0; i < numFaces; ++i) {
      const thisFaceVertexNormals = [];
      // get the normal for this face
      const thisFaceNormal = faceNormals[i];
      // for each vertex on the face
      for (let j = 0; j < 3; ++j) {
        const ndx = getNextIndex();
        const sharedNdx = vertIndices[ndx];
        const faces = vertFaces[sharedNdx];
        const norm = [0, 0, 0];
        faces.forEach(faceNdx => {
          // is this face facing the same way
          const otherFaceNormal = faceNormals[faceNdx];
          const dot = m4.dot(thisFaceNormal, otherFaceNormal);
          if (dot > maxAngleCos) {
            m4.addVectors(norm, otherFaceNormal, norm);
          }
        });
        m4.normalize(norm, norm);
        const poffset = ndx * 3;
        const toffset = ndx * 2;
        newVertIndices.push(getNewVertIndex(
            positions[poffset + 0], positions[poffset + 1], positions[poffset + 2],
            norm[0], norm[1], norm[2],
            texcoords[toffset + 0], texcoords[toffset + 1]));
      }
    }

    return {
      position: newPositions,
      texcoord: newTexcoords,
      normal: newNormals,
      indices: newVertIndices,
    };

  }

  webglLessonsUI.setupUI(document.querySelector("#ui"), data, [
    { type: "slider",   key: "distance",   change: update, min: 0.001, max: 5,              precision: 3, step: 0.001, },
    { type: "slider",   key: "divisions",  change: update, min: 1    , max: 60,                                        },
    { type: "slider",   key: "startAngle", change: update, min: 0    , max: Math.PI * 2,    precision: 3, step: 0.001, uiMult: 180 / Math.PI, uiPrecision: 0 },
    { type: "slider",   key: "endAngle",   change: update, min: 0    , max: Math.PI * 2,    precision: 3, step: 0.001, uiMult: 180 / Math.PI, uiPrecision: 0 },
    { type: "slider",   key: "maxAngle",   change: update, min: 0.001, max: Math.PI,        precision: 3, step: 0.001, uiMult: 180 / Math.PI, uiPrecision: 1 },
    { type: "checkbox", key: "capStart",   change: update },
    { type: "checkbox", key: "capEnd",     change: update },
    { type: "checkbox", key: "triangles",  change: render },
    { type: "option",   key: "mode",       change: render, options: [ "normals", "lit", "texcoords" ], },
  ]);

  /* eslint brace-style: 0 */
  gl.canvas.addEventListener('mousedown', (e) => { e.preventDefault(); startRotateCamera(e); });
  window.addEventListener('mouseup', stopRotateCamera);
  window.addEventListener('mousemove', rotateCamera);
  gl.canvas.addEventListener('touchstart', (e) => { e.preventDefault(); startRotateCamera(e.touches[0]); });
  window.addEventListener('touchend', (e) => { stopRotateCamera(e.touches[0]); });
  window.addEventListener('touchmove', (e) => { rotateCamera(e.touches[0]); });

  let lastPos;
  let moving;
  function startRotateCamera(e) {
    lastPos = getRelativeMousePosition(gl.canvas, e);
    moving = true;
  }

  function rotateCamera(e) {
    if (moving) {
      const pos = getRelativeMousePosition(gl.canvas, e);
      const size = [4 / gl.canvas.width, 4 / gl.canvas.height];
      const delta = v2.mult(v2.sub(lastPos, pos), size);

      // this is bad but it works for a basic case so phffttt
      worldMatrix = m4.multiply(m4.xRotation(delta[1] * 5), worldMatrix);
      worldMatrix = m4.multiply(m4.yRotation(delta[0] * 5), worldMatrix);

      lastPos = pos;

      render();
    }
  }

  function stopRotateCamera(e) {
    moving = false;
  }

  function degToRad(deg) {
    return deg * Math.PI / 180;
  }

  function clamp(v, min, max) {
    return Math.max(Math.min(v, max), min);
  }

  function lerp(a, b, t) {
    return a + (b - a) * t;
  }

  function getQueryParams() {
    var params = {};
    if (window.location.search) {
      window.location.search.substring(1).split("&").forEach(function(pair) {
        var keyValue = pair.split("=").map(function(kv) {
          return decodeURIComponent(kv);
        });
        params[keyValue[0]] = keyValue[1];
      });
    }
    return params;
  }

  function getRelativeMousePosition(canvas, e) {
    const rect = canvas.getBoundingClientRect();
    const x = (e.clientX - rect.left) / (rect.right  - rect.left) * canvas.width;
    const y = (e.clientY - rect.top ) / (rect.bottom - rect.top ) * canvas.height;
    return [
      (x - canvas.width  / 2) / window.devicePixelRatio,
      (y - canvas.height / 2) / window.devicePixelRatio,
    ];
  }

  // creates a texture info { width: w, height: h, texture: tex }
  // The texture will start with 1x1 pixels and be updated
  // when the image has loaded
  function loadImageAndCreateTextureInfo(url, callback) {
    var tex = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, tex);
    // Fill the texture with a 1x1 blue pixel.
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE,
                  new Uint8Array([0, 0, 255, 255]));

    var textureInfo = {
      width: 1,   // we don't know the size until it loads
      height: 1,
      texture: tex,
    };
    var img = new Image();
    img.addEventListener('load', function() {
      textureInfo.width = img.width;
      textureInfo.height = img.height;

      gl.bindTexture(gl.TEXTURE_2D, textureInfo.texture);
      gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img);

      // Check if the image is a power of 2 in both dimensions.
      if (isPowerOf2(img.width) && isPowerOf2(img.height)) {
         // Yes, it's a power of 2. Generate mips.
         gl.generateMipmap(gl.TEXTURE_2D);
      } else {
         // No, it's not a power of 2. Turn of mips and set wrapping to clamp to edge
         gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
         gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
         gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
      }

      if (callback) {
        callback();
      }
    });
    img.src = url;

    return textureInfo;
  }

  function isPowerOf2(value) {
    return (value & (value - 1)) === 0;
  }
}

const v2 = (function() {
  // adds 1 or more v2s
  function add(a, ...args) {
    const n = a.slice();
    [...args].forEach(p => {
      n[0] += p[0];
      n[1] += p[1];
    });
    return n;
  }

  function sub(a, ...args) {
    const n = a.slice();
    [...args].forEach(p => {
      n[0] -= p[0];
      n[1] -= p[1];
    });
    return n;
  }

  function mult(a, s) {
    if (Array.isArray(s)) {
      let t = s;
      s = a;
      a = t;
    }
    if (Array.isArray(s)) {
      return [
        a[0] * s[0],
        a[1] * s[1],
      ];
    } else {
      return [a[0] * s, a[1] * s];
    }
  }

  function lerp(a, b, t) {
    return [
      a[0] + (b[0] - a[0]) * t,
      a[1] + (b[1] - a[1]) * t,
    ];
  }

  function min(a, b) {
    return [
      Math.min(a[0], b[0]),
      Math.min(a[1], b[1]),
    ];
  }

  function max(a, b) {
    return [
      Math.max(a[0], b[0]),
      Math.max(a[1], b[1]),
    ];
  }

  // compute the distance squared between a and b
  function distanceSq(a, b) {
    const dx = a[0] - b[0];
    const dy = a[1] - b[1];
    return dx * dx + dy * dy;
  }

  // compute the distance between a and b
  function distance(a, b) {
    return Math.sqrt(distanceSq(a, b));
  }

  // compute the distance squared from p to the line segment
  // formed by v and w
  function distanceToSegmentSq(p, v, w) {
    const l2 = distanceSq(v, w);
    if (l2 === 0) {
      return distanceSq(p, v);
    }
    let t = ((p[0] - v[0]) * (w[0] - v[0]) + (p[1] - v[1]) * (w[1] - v[1])) / l2;
    t = Math.max(0, Math.min(1, t));
    return distanceSq(p, lerp(v, w, t));
  }

  // compute the distance from p to the line segment
  // formed by v and w
  function distanceToSegment(p, v, w) {
    return Math.sqrt(distanceToSegmentSq(p, v, w));
  }

  return {
    add: add,
    sub: sub,
    max: max,
    min: min,
    mult: mult,
    lerp: lerp,
    distance: distance,
    distanceSq: distanceSq,
    distanceToSegment: distanceToSegment,
    distanceToSegmentSq: distanceToSegmentSq,
  };
}());

main();
</script>
</html>

